﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Text;
using System.Windows.Forms;

namespace MosCraft.Utilities.FileIO
{
    /// <summary>
    /// This exception is thrown whenever the specified section does not exist.
    /// </summary>
    public class SectionDoesNotExistException : Exception
    {
        private string _msg = string.Empty;
        private string _section = string.Empty;

        public override string Message
        {
            get { return string.Format("The section \"{0}\" does not exist!", _section); }
        }

        public SectionDoesNotExistException(string section)
        {
            _section = section;
        }
    }

    /// <summary>
    /// This exception is thrown whenever the specified field inside a section does not exist.
    /// </summary>
    public class FieldDoesNotExistException : Exception
    {
        private string _msg = string.Empty;
        private string _section = string.Empty;
        private string _field = string.Empty;

        public override string Message
        {
            get { return string.Format("The field \"{0}\" does not exist in the section \"{1}\"!", _field, _section); }
        }

        public FieldDoesNotExistException(string section, string field)
        {
            _section = section;
            _field = field;
        }
    }


    /// <summary>
    /// This structure contains the INI data read from the file.
    /// </summary>
    public struct Node
    {
        /// <summary>
        /// The field name of this item.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// The value of the object.
        /// 
        /// The type of value will either be:
        /// 1) String
        /// 2) Boolean
        /// 3) Decimal
        /// </summary>
        public object Value { get; set; }
    }

    /// <summary>
    /// An easy to use, managed class to create, read, write, and modify INI files.
    /// </summary>
    public class XMLSettingsHandler
    {
        #region Properties

        /// <summary>
        /// Internal use, used for making sure sections and their respective items are kept organized.
        /// </summary>
        private Dictionary<string, List<Node>> Items { get; set; }

        /// <summary>
        /// Get the different sections in the INI file.
        /// </summary>
        public IEnumerable<string> Sections { get { return Items.Keys.ToList(); } }

        #endregion

        #region Initialization and Getter

        /// <summary>
        /// Creates a new INI class.
        /// </summary>
        public XMLSettingsHandler()
        {
            Items = new Dictionary<string, List<Node>>();
        }

        /// <summary>
        /// A public accessor to retrieve items based on their section
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public List<Node> this[string name]
        {
            get
            {
                if (!SectionExists(name))
                    AddSection(name);
                return Items[name];
            }
            set
            {
                if (!SectionExists(name))
                    AddSection(name);
                Items[name] = value;
            }
        }
        #endregion

        #region Add & Remove
        /// <summary>
        /// Adds a new section to the INI file.
        /// </summary>
        /// <param name="section">The name of the section to add.</param>
        public void AddSection(string section)
        {
            if (!SectionExists(section))
                Items.Add(section, new List<Node>());
        }

        /// <summary>
        /// Adds a new field to the INI file.
        /// </summary>
        /// <param name="section">The section to add the field under.</param>
        /// <param name="field">The name of the field.</param>
        /// <param name="value">The value to add. The value must be either a number, a boolean, or a string.</param>
        public void Add(string section, string field, object value)
        {
            if (!SectionExists(section) || (FieldExists(section, field))) return;
            this[section].Add(new Node { Name = field, Value = value });
        }

        /// <summary>
        /// Removes a section from the INI file.
        /// </summary>
        /// <param name="section">The name of the section to remove.</param>
        public void Remove(string section)
        {
            if (!SectionExists(section)) return;
            Items.Remove(section);
        }

        /// <summary>
        /// Removes a field from a specified section in the INI file.
        /// </summary>
        /// <param name="section">The name of the section to look under.</param>
        /// <param name="field">The name of the field to remove.</param>
        public void Remove(string section, string field)
        {
            if (!SectionExists(section) || !FieldExists(section, field)) return;
            var itemList = this[section];
            for (var i = 0; i < itemList.Count; i++)
            {
                if (itemList[i].Name.Equals(field))
                {
                    itemList.RemoveAt(i);
                }
            }
            this[section] = itemList;
        }

        #endregion

        #region Existance Checking

        /// <summary>
        /// Checks to see if a section exists.
        /// </summary>
        /// <param name="section">The name of the section to check.</param>
        /// <returns>Boolean</returns>
        private bool SectionExists(string section)
        {
            return Sections.Where(sectionName => sectionName.Equals(section)).Count().Equals(1);
        }

        /// <summary>
        /// Checks to see if a field inside of a section exists.
        /// </summary>
        /// <param name="section">The name of the section to check under.</param>
        /// <param name="field">The name of the field to look for.</param>
        /// <returns>Boolean</returns>
        private bool FieldExists(string section, string field)
        {
            return SectionExists(section) && this[section].Where(fieldName => fieldName.Name.Equals(field)).Count().Equals(1);
        }

        #endregion

        #region Get

        /// <summary>
        /// Returns the value of a field in the specified section.
        /// </summary>
        /// <param name="section">The specified section to look under.</param>
        /// <param name="field">The field to look for.</param>
        /// <returns>Object</returns>
        private object _GetField(string section, string field)
        {
            // LINQ magic.
            if (SectionExists(section) && FieldExists(section, field))
            {
                // Grab the field and return it.
                return this[section].Where(fieldName => fieldName.Name.Equals(field)).ToList()[0].Value;
            }

            throw new SectionDoesNotExistException(section);
        }

        public void GetField(string section, string field, out bool value)
        {
            value = Convert.ToBoolean(_GetField(section, field));
        }

        public void GetField(string section, string field, out decimal value)
        {
            value = Convert.ToDecimal(_GetField(section, field));
        }

        public void GetField(string section, string field, out float value)
        {
            value = Single.Parse(_GetField(section, field).ToString());
        }

        public void GetField(string section, string field, out double value)
        {
            value = Double.Parse( _GetField(section, field).ToString());
        }

        public void GetField(string section, string field, out string value)
        {
            value = _GetField(section, field).ToString();
        }

        #endregion

        #region Set

        /// <summary>
        /// Sets the value of the specified field.
        /// </summary>
        /// <param name="section">The section to look under.</param>
        /// <param name="field">The field to look for.</param>
        /// <param name="value">The value to set.</param>
        private void _SetField(string section, string field, object value)
        {
            // Check to see that the field and section exists
            if (!SectionExists(section)) throw new SectionDoesNotExistException(section);
            if (!FieldExists(section, field)) throw new FieldDoesNotExistException(section, field);

            // Grab the field list
            var fieldList = this[section];

            // Loop
            for (var i = 0; i < fieldList.Count; i++)
            {

                //Is this it? If not, continue.
                if (!fieldList[i].Name.Equals(field)) continue;

                // Grab the field
                var fieldData = fieldList[i];
                fieldData.Value = value;
                fieldList[i] = fieldData;

            }

            // Set it back
            this[section] = fieldList;
        }

        /// <summary>
        /// Sets the value of the specified field to a boolean value.
        /// </summary>
        /// <param name="section">The section to look under.</param>
        /// <param name="field">The field to look for.</param>
        /// <param name="value">The value to set.</param>
        public void SetField(string section, string field, bool value)
        {
            _SetField(section, field, value);
        }

        /// <summary>
        /// Sets the value of the specified field to a numerical value.
        /// </summary>
        /// <param name="section">The section to look under.</param>
        /// <param name="field">The field to look for.</param>
        /// <param name="value">The value to set.</param>
        public void SetField(string section, string field, decimal value)
        {
            _SetField(section, field, value);
        }

        /// <summary>
        /// Sets the value of the specified field to a string value.
        /// </summary>
        /// <param name="section">The section to look under.</param>
        /// <param name="field">The field to look for.</param>
        /// <param name="value">The value to set.</param>
        public void SetField(string section, string field, string value)
        {
            _SetField(section, field, value);
        }

        #endregion

        #region Save and Load
        /// <summary>
        /// Loads a XML and processes it
        /// </summary>
        /// <param name="xmlFilePath">file</param>
        public void Load(string xmlFilePath)
        {
            XDocument XMLreader = XDocument.Load(xmlFilePath);

            var sections = from section in XMLreader.Descendants("Section")
                select new
                {
                    Header = section.Attribute("Name").Value,
                    Children = section.Descendants("Config")
                };

            foreach (var section in sections)
            {
                if (!SectionExists(section.Header)) AddSection(section.Header);

                foreach (var configLine in section.Children)
                {
                    string configName = configLine.Attribute("Name").Value;
                    string configValue = configLine.Attribute("Value").Value;

                    // Try some conversions to store the item as their natural format.
                    bool boolTest;
                    decimal numTest;

                    // Boolean test
                    if (Boolean.TryParse(configValue, out boolTest))
                    {
                        this[section.Header].Add(new Node { Name = configName, Value = boolTest });
                        continue;
                    }

                    // Number test
                    if (Decimal.TryParse(configValue, out numTest))
                    {
                        this[section.Header].Add(new Node { Name = configName, Value = numTest });
                        continue;
                    }

                    // It's a string, add it and keep going.
                    this[section.Header].Add(new Node { Name = configName, Value = configValue });
                }
            }
        }

        /// <summary>
        /// Saves to a file
        /// </summary>
        /// <param name="iniFile"></param>
        public void Save(string xmlFilePath)
        {
            XElement xmlElement = new XElement("UserSettings");

            foreach (var section in Sections)
            {
                XElement sectionNode = new XElement("Section");
                sectionNode.SetAttributeValue("Name", section);

                var items = this[section];
                
                foreach (var item in items)
                {
                    XElement XNode = new XElement("Config");
                    XNode.SetAttributeValue("Name", item.Name);
                    XNode.SetAttributeValue("Value", item.Value);
                    sectionNode.Add(XNode);
                }

                xmlElement.Add(sectionNode);
            }

            xmlElement.Save(xmlFilePath);
        }
        #endregion

    }
}
